<?php

namespace 中文汇编器;

require_once "伪指令.class.php";
require_once "rv32i.class.php";

class 预处理
{
	private $工程输入文件;
	private $工程段排序;
	private $工程起始地址;
	private $当前段 = [];
	private $当前文件 = [];
    private $全局标签表 = [];
    private $局部标签表 = [];
    private $段信息 = [];
    private $中间代码 = [];
	private $指令暂存 = [];
    private $工程预设段基址;

    public function __construct($输入文件, $段排序, $程序起始地址, $段基址)
    {
        $this->段信息初始化($输入文件, $段排序, $程序起始地址, $段基址);
        $fileId = 0;
        
        foreach ($this->工程输入文件 as $inFile)
        {
            if (file_exists($inFile) == false)
                exit('错误：文件【' . $inFile . '】不存在');

            $this->当前文件["文件名"] = $inFile;
            $this->当前文件["序号"] = $fileId;
            $this->当前文件["行数"] = 0;
            
            $inFileHandle = fopen($inFile, 'r');
            while (!feof($inFileHandle))
            {
                $this->当前文件["行数"] = $this->当前文件["行数"] + 1;
           
                //程序行处理
                $line = trim(fgets($inFileHandle));
                $firstChar = mb_substr($line, 0, 1);
            
                //处理注释
                //如果第一个字符是#或是空行，则直接下一行
                if ($firstChar == '#' or $firstChar == '') continue;
            
                //如果行内有#，则以#为界线进行字符串分割，取第一块
                if (mb_substr_count($line, '#') != 0) 
                {
                    $splitArray = mb_split('#', $line);
                    $line = trim($splitArray[0]);
                }
                
                switch ($firstChar)
                {
                    case '@': 
                        $this->指示符处理($line); 
                        break;
                    case '《': 
                        $this->段标题处理($line);
                        break;
                    default:
                        if (mb_substr($line, mb_strlen($line)-1, 1) == '：') 
							$this->标签处理(mb_substr($line, 0, mb_strlen($line)-1));
                        else
                        {
                            if ($this->当前文件["行数"] == 1)
                                exit('错误：' . $this->当前文件["文件名"] . '必须以段标题为起始');
                            $in = ["文件序号" => $this->当前文件["序号"],
                                   "文件名" =>	$this->当前文件["文件名"],
                                   "行数" => $this->当前文件["行数"],
                                   "类型" => '指令',
                                   "内容" => $line,
                                   "长度" => 4];
                            $this->指令暂存[$this->当前段["序号"]][] = $in;
                            $this->当前段["长度"] = $this->当前段["长度"] + $this->长度处理($in);
                        }

                }
            }
            fclose($inFileHandle);
            $fileId = $fileId + 1;
        }
		$this->段长重设();
        //echo "基址重设之后：\n";
        //print_r($this->指令暂存);
        //print_r($this->全局标签表);
        //print_r($this->局部标签表);
        //print_r($this->段信息);
		$this->暂存处理();
		//echo "暂存处理之后：\n";
		//print_r($this->中间代码);
    }
    
    public function 获取全局标签表()
    {
        return $this->全局标签表;
    }
    
    public function 获取局部标签表()
    {
        return $this->局部标签表;
    }
    
    public function 获取中间代码()
    {
        return $this->中间代码;
    }

    public function 获取段信息()
    {
        return $this->段信息;
    }
    
	private function 段长重设()
	{
		//预处理结束后，将每一段的基址按照段排序重新设定
		/*
        $n = count($this->工程段排序);
        for ($i = 1; $i < $n; $i++)
        {
            if ($this->当前段["序号"] == ($i-1))
            {
                $this->段信息[$i - 1]["长度"] = $this->段信息[$i - 1]["长度"] + $this->当前段["长度"];
                $this->段信息[$i]["基址"] = $this->工程预设段基址[$i];
                if ($this->段信息[$i]["基址"] < $this->段信息[$i - 1]["长度"])
                    exit("错误：《" . $this->段信息[$i]["标题"] . "》段超长");
                //$this->段信息[$i]["基址"] = $this->段信息[$i - 1]["长度"] + $this->段信息[$i - 1]["基址"];
                $this->当前段["长度"] = 0;
            }
            else
            {
                $this->段信息[$i]["基址"] = $this->工程预设段基址[$i];
                if ($this->段信息[$i]["基址"] < $this->段信息[$i - 1]["长度"])
                    exit("错误：《" . $this->段信息[$i]["标题"] . "》段超长");
                //$this->段信息[$i]["基址"] = $this->段信息[$i - 1]["长度"] + $this->段信息[$i - 1]["基址"];
            }
        }

        //由于当前段只是某个文件中的某个段，因此段信息中的长度应该是所有具有相同序号的段的当前段长度之和
        $this->段信息[$i - 1]["长度"] = $this->当前段["长度"] + $this->段信息[$i - 1]["长度"];
        //print_r($this->段信息);



		$n = count($this->全局标签表);
        for ($i = 0; $i < $n; $i++)
			$this->全局标签表[$i]["段基址"] = $this->段信息[$this->全局标签表[$i]["段序号"]]["基址"];

		$n = count($this->局部标签表);
        for ($i = 0; $i < $n; $i++)
			$this->局部标签表[$i]["段基址"] = $this->段信息[$this->局部标签表[$i]["段序号"]]["基址"];
        */
        $n = count($this->工程段排序);
        for ($i = 1; $i < $n; $i++)
        {
            if ($this->当前段["序号"] == ($i - 1))
                $this->段信息[$i - 1]["长度"] = $this->段信息[$i - 1]["长度"] + $this->当前段["长度"];

            if ($this->段信息[$i]["基址"] < $this->段信息[$i - 1]["长度"])
                exit("错误：《" . $this->段信息[$i]["标题"] . "》段超长");

            $this->当前段["长度"] = 0;
        }
        $this->段信息[$i - 1]["长度"] = $this->当前段["长度"] + $this->段信息[$i - 1]["长度"];

	}

    private function 段信息初始化($输入文件, $段排序, $程序起始地址, $段基址)
    {
        $this->工程输入文件 = $输入文件;
        $this->工程段排序 = $段排序;
        $this->工程起始地址 = $程序起始地址;
        $this->工程预设段基址 = $段基址;

        foreach ($段基址 as $secAddr)
        {
            $chr2 = mb_substr($secAddr, 0, 2);
            if ($chr2 == '0x' or $chr2 == '0b')
            {
                $chrOther2 = mb_substr($secAddr, 2);
                if (is_numeric($chrOther2) == false)
                    exit('错误：工程文件中【$段基址】配置错误');
            }

            $chr1 = mb_substr($secAddr, 0, 1);
            if ($chr1 == '0')
            {
                $chrOther1 = mb_substr($secAddr, 1);
                if (is_numeric($chrOther1) == false)
                    exit('错误：工程文件中【$段基址】配置错误');
            }

            if (is_numeric($secAddr) == false)
                exit('错误：工程文件中【$段基址】配置错误');

        }

        if (count($段排序) != count($段基址))
            exit("错误：工程文件中段基址配置错误");

        for ($i = 1; $i < count($段基址); $i++)
            if ($段基址[$i] < $段基址[$i-1])
                exit("错误：工程文件中段基址应升序排列");

        $n = count($this->工程段排序);
        for ($i = 0; $i < $n; $i++)
            $this->段信息[$i] = ["标题" => $段排序[$i], "基址" => $段基址[$i], "长度" => 0x0];
    }
    
    private function 段标题处理($line)
    {
        //如果当前段不为空，则证明不是第一个段，因此需要将当前段信息写入到段信息中
        if ($this->当前段 != [])
        {
            $section["标题"] = $this->当前段["标题"];
            $section["基址"] = $this->工程预设段基址[$this->当前段["序号"]];
			//段信息中的段长度不一定为0，这是要把每个文件中的相同段合并到一起
            $section["长度"] = $this->当前段["长度"] + $this->段信息[$this->当前段["序号"]]["长度"];
            $this->段信息[$this->当前段["序号"]] = $section;
            //print_r($this->段信息);
        }
        
        $段序号 = -1;
        $sectionName = mb_substr($line, 1, mb_strlen($line)-2);
        $i = 0;
        foreach ($this->工程段排序 as $item)
        {
            if ($item == $sectionName)
            {
                $段序号 = $i;
                break;
            }
            else 
            {
                $i = $i + 1;
                continue;
            }
        }
        if ($段序号 == -1) 
			exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '工程文件未配置该段');
        //当前段在每次处理段标题时长度都会被清0
        $this->当前段 = ["序号" => $段序号, "标题" => $sectionName, "长度" => 0x0];
    }
    
    private function 标签处理($label)
    {
        //首先寻找全局标签，全局标签一定是使用“@全局标签”预先声明过的，因此，只需要判断是否已经初始化过了
        //如果初始化过，则证明已经定义了，如果没有，则证明只是声明过，还未正式定义
        $n = count($this->全局标签表);
        for ($i = 0; $i < $n; $i++)
        {
            if ($label == $this->全局标签表[$i]["标签名"]) 
            {
                if ($this->当前段["标题"] != '指令')
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '全局标签必须在《指令》段定义');
                if ($this->全局标签表[$i]["初始化"] == false)
                {
                    $this->全局标签表[$i]["偏移"] = $this->段信息[$this->当前段["序号"]]["长度"] + $this->当前段["长度"];
                    $this->全局标签表[$i]["初始化"] = true;
                    //print_r($this->当前段);
                    return;
                }
                else 
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能重复定义全局标签【' . $label . '】');
            }
        }
        //print_r($this->全局标签表);
        //如果全局标签表未命中，则为局部标签，如果已经在局部标签表中，就是重复定义
        //如果不在表中，则建立新的局部标签项，标签名改为“_文件序号_标签名”
        $n = count($this->局部标签表);
        for ($i = 0; $i < $n; $i++)
        {
            if ($label == $this->局部标签表[$i]["标签名"]) 
                exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能重复定义局部标签【' . $label . '】');
        }
  
        $lLabel["标签名"] = '_' . $this->当前文件["序号"] . '_' . $label;
        $lLabel["段序号"] = $this->当前段["序号"];
        $lLabel["段基址"] = $this->工程预设段基址[$this->当前段["序号"]];
        $lLabel["偏移"] = $this->段信息[$this->当前段["序号"]]["长度"] + $this->当前段["长度"];
        //print_r($this->当前段);
        $this->局部标签表[] = $lLabel;
    }
    
    private function 指示符处理($line)
    {
        $splitArray = mb_split('，', $line);
        switch ($splitArray[0])
        {
            case '@全局标签':
            {
                if ($this->当前段 == []) 
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能在段外定义标签');
                for ($i = 1; $i < count($splitArray); $i++)
                {
                    $gLabel["标签名"] = $splitArray[$i];
                    $gLabel["段序号"] = $this->当前段["序号"];
                    $gLabel["段基址"] = $this->工程预设段基址[$this->当前段["序号"]];
                    $gLabel["偏移"] = 0x0;
                    $gLabel["初始化"] = false;
                    $this->全局标签表[] = $gLabel;
                }
                //print_r($this->全局标签表);
                break;
            }
            case '@字符串':
            {
                if ($this->当前段["标题"] == '指令')
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能在《指令》段定义数据');
				$chrArray = unpack("C*", $splitArray[1]);
				//print_r($chrArray);

                //判断字符串是否4字节对齐，不齐则用nul补齐
				//取字符串的字节数，除以4取整，再乘以4得出最近的4的倍数，再加4得出对齐的数字
				$chrNum = count($chrArray);
				$nullNum = intdiv($chrNum, 4) * 4 + 4 - $chrNum;

				//每个字符串的第一个4字节是字符串的长度
				$str = pack("V", $chrNum);
				$str = $str . $splitArray[1];

				for ($i = 0; $i < $nullNum; $i++)
                {
                    $str = $str . chr(0);
                }

				$length = $chrNum + $nullNum + 4;
                $this->当前段["长度"] = $this->当前段["长度"] + $length;
                $this->指令暂存[$this->当前段["序号"]][] = ["文件名" => $this->当前文件["文件名"],
				                     "行数" => $this->当前文件["行数"],
									 "类型" => '指示符',
				                     "内容" => $str,
                                     "长度" => $length];
                //var_dump($this->指令暂存);
                break;
            }
            case '@数字':		//@数字，123，-234，0x7
			{
                if ($this->当前段["标题"] == '指令')
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能在《指令》段定义数据');
				$numStr = '';
				$length = 0;
				for ($i = 1; $i < count($splitArray); $i++)
				{
					$num = intval($splitArray[$i], 0);
					$numStr = $numStr . pack("l*", $num);
					$length = $length + 4;
				}
				$this->当前段["长度"] = $this->当前段["长度"] + $length;
				$this->指令暂存[$this->当前段["序号"]][] = ["文件名" => $this->当前文件["文件名"], 
				                     "行数" => $this->当前文件["行数"],
									 "类型" => '指示符',
				                     "内容" => $numStr,
                                     "长度" => $length];
				break;
			}
            case '@字节填充':		//@字节填充，十六进制两位数字
			{
                if ($this->当前段["标题"] == '指令')
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能在《指令》段定义数据');
				$numStr = '';
				$len = 0;
				for ($i = 1; $i < count($splitArray); $i++)
				{
					$num = intval($splitArray[$i], 0);
					$numStr = $numStr . chr($num);
					$len = $len + 1;
				}
				
				$length = intdiv($len, 4) * 4 + 4;
                for ($i = 0; $i < ($length-$len); $i++)
                    $numStr = $numStr . chr(0x00);
				$this->当前段["长度"] = $this->当前段["长度"] + $length;
				$this->指令暂存[$this->当前段["序号"]][] = ["文件名" => $this->当前文件["文件名"], 
				                     "行数" => $this->当前文件["行数"],
									 "类型" => '指示符',
				                     "内容" => $numStr,
                                     "长度" => $length];
				break;
			}
            case '@字节用0填充':		//@字节用0填充，40
			{
                if ($this->当前段["标题"] == '指令')
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能在《指令》段定义数据');
				$inNum = intval($splitArray[1], 0);
                $length = $inNum;
				if ($inNum % 4 != 0)
					$length = intdiv($inNum, 4) * 4 + 4;

				$arg = '@' . $length;
				$numStr = pack($arg);
				$this->当前段["长度"] = $this->当前段["长度"] + $length;
				$this->指令暂存[$this->当前段["序号"]][] = ["文件名" => $this->当前文件["文件名"], 
				                     "行数" => $this->当前文件["行数"],
									 "类型" => '指示符',
				                     "内容" => $numStr,
                                     "长度" => $length];
				break;
			}
            case '@字节用1填充':
			{
                if ($this->当前段["标题"] == '指令')
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能在《指令》段定义数据');
				$inNum = intval($splitArray[1], 0);
                $length = $inNum;
				if ($inNum % 4 != 0)
					$length = intdiv($inNum, 4) * 4 + 4;
				
				$numStr = '';
				for ($i = 0; $i < $inNum; $i++)
					$numStr = $numStr . chr(0xff);
                for ($i = 0; $i < ($length-$inNum); $i++)
                    $numStr = $numStr . chr(0x00);
				$this->当前段["长度"] = $this->当前段["长度"] + $length;
				$this->指令暂存[$this->当前段["序号"]][] = ["文件名" => $this->当前文件["文件名"], 
				                     "行数" => $this->当前文件["行数"],
									 "类型" => '指示符',
				                     "内容" => $numStr,
                                     "长度" => $length];
				break;
			}
			case '@分配内存':
			{
                if ($this->当前段["标题"] == '指令')
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能在《指令》段定义数据');
				$inNum = intval($splitArray[1], 0);
				$length = $inNum;
				if ($inNum % 4 != 0)
					$length = intdiv($inNum, 4) * 4 + 4;
                $numStr = '';
                for ($i = 0; $i < $length; $i++)
					$numStr = $numStr . chr(0x0);
				$this->当前段["长度"] = $this->当前段["长度"] + $length;
				$this->指令暂存[$this->当前段["序号"]][] = ["文件名" => $this->当前文件["文件名"], 
				                     "行数" => $this->当前文件["行数"],
									 "类型" => '指示符',
				                     "内容" => $numStr,
                                     "长度" => $length];
				break;
            }
            case '@数字常量':
            {
                if ($this->当前段["标题"] == '指令')
                    exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '不能在《指令》段定义数据');
                $inNum = intval($splitArray[1], 0);
                $inNum = pack("l", $inNum);
                $this->当前段["长度"] = $this->当前段["长度"] + 4;
                $this->指令暂存[$this->当前段["序号"]][] = ["文件名" => $this->当前文件["文件名"],
                    "行数" => $this->当前文件["行数"],
                    "类型" => '指示符',
                    "内容" => $inNum,
                    "长度" => 4];
                break;
            }
			default:
                exit('错误：' . $this->当前文件["文件名"] . '的第【' . $this->当前文件["行数"] . '】行，' . '没有【' . $splitArray[0] . '】这个指示符');
        }
    }

    private function 长度处理($in)
    {
        $pseIns = new 伪指令($this->局部标签表, $this->全局标签表);
        $ins = new rv32i($this->局部标签表, $this->全局标签表);
        //print_r($in);
		$opItem = mb_split('，', $in["内容"]);
		//var_dump($opItem);
		if ($pseIns->判断($opItem[0]) == true)
		{
            $pseIns->置长度位(1);
		    $insStr = $pseIns->翻译($in);
		    if (is_array($insStr) == false)
		        return(4);
		    else
		        return(4 * count($insStr));
		}

		if ($ins->判断($opItem[0]) == true)
		    return(4);

        exit('错误：' . $in["文件名"] . '的第【' . $in["行数"] . '】行，' . '没有【' . $opItem[0] . '】这个指令名');
    }

    private function 暂存处理()
    {
        //echo "\n", "指令暂存的处理\n";
		$pseIns = new 伪指令($this->局部标签表, $this->全局标签表);
		$ins = new rv32i($this->局部标签表, $this->全局标签表);

		//var_dump($this->指令暂存);
		$n = count($this->指令暂存);
		//var_dump($n);
		for ($i = 0; $i < $n; $i++)
		{
			$段指令暂存 = $this->指令暂存[$i];
			$m = count($段指令暂存);
			//echo "段指令暂存\n";
			//var_dump($段指令暂存);

			for ($j = 0; $j < $m; $j++)
			{
				if ($段指令暂存[$j]["类型"] == '指示符')
				{
					$this->中间代码[] = ["内容" => $段指令暂存[$j]["内容"],
                                        "长度" => $段指令暂存[$j]["长度"],
                                        "类型" => '指示符',
                                        "段基址" => $this->段信息[$i]["基址"],
                                        "段序号" => $i];
					//var_dump("中间代码\n", $this->中间代码);
					continue;
				}
			
				if ($段指令暂存[$j]["类型"] == '指令')
				{
					$opItem = mb_split('，', $段指令暂存[$j]["内容"]);
                    //var_dump($opItem);
					if ($pseIns->判断($opItem[0]) == true)
					{
						$insStr = $pseIns->翻译($段指令暂存[$j]);
						if (is_array($insStr) == false)
						{
                            $this->中间代码[] = ["内容" => $insStr,
                                                "长度" => 4,
                                                "类型" => '指令',
                                                "段基址" => $this->段信息[$i]["基址"],
                                                "段序号" => $i];
                        }
						else
						{
							for ($k = 0; $k < count($insStr); $k++)
                                $this->中间代码[] = ["内容" => $insStr[$k],
                                                    "长度" => 4,
                                                    "类型" => '指令',
                                                    "段基址" => $this->段信息[$i]["基址"],
                                                    "段序号" => $i];
						}
						continue;
					}
				
					if ($ins->判断($opItem[0]) == true)
					{
                        $this->中间代码[] = ["内容" => $ins->标签处理($段指令暂存[$j]),
                                            "长度" => 4,
                                            "类型" => '指令',
                                            "段基址" => $this->段信息[$i]["基址"],
                                            "段序号" => $i];
                        continue;
                    }

                    exit('错误：' . $段指令暂存[$j]["文件名"] . '的第【' . $段指令暂存[$j]["行数"] . '】行，' . '没有【' . $opItem[0] . '】这个指令名');
                }
			}
			//print_r($this->中间代码);
		}
    }
    
}

?>
